package com.t360.numberencoding;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Given a dictionary and a mapping, this class operates on the numbers finding and
 * printing the possible combinations between a provided number and the words in the
 * dictionary, according to the provided mapping
 * @author fmanganiello
 *
 */
public class Combination {
	private Dictionary dictionary;
	private Mapping mapping;
	
	public Combination(final Dictionary dictionary, final Mapping mapping) {
		this.dictionary = dictionary;
		this.mapping = mapping;
	}
	
	/**
	 * Get the strings associated to a number, according to the provided dictionary and mapping
	 * @param number
	 * @return
	 */
	public List<String> getNumberAssociations(final String number) {
		ArrayList<String> combinations = new ArrayList<String>();
		ArrayList<String> associations = new ArrayList<String>();
		
		// The normalized form of the input number, removing non-numeric characters
		String normalizedNumber = normalizeNumber(number);
		
		// Return all the possible combinations of the number, according to the
		// provided mapping, in the array "combinations"
        numberCombinations(normalizedNumber, 0, new AtomicReference<StringBuffer>(new StringBuffer()), combinations);
        
        for (String combination : combinations) {
        	StringBuffer curString = new StringBuffer();
        	boolean wasDigitUsed = false;
        	
        	// Iterate over each of the possible combination of letters of the input number
        	for (int index=0; index < combination.length(); ++index) {
        		int len = 0;
        		for (len = combination.length()-index; len > 0; --len) {
					String substring = combination.substring(index, index+len);
					
					// Check whether the current substring is in the dictionary
	        		String dictValue = dictionary.get(substring);
	        		if (dictValue != null) {
	        			wasDigitUsed = false;
	        			
	        			// If so, append its dictionary value (it does not necessarily match
	        			// the substring) 
	        			curString.append(dictValue + " ");
	        			
	        			// Move the index forward, if there is still part of the combination
	        			// that needs to be evaluated
	        			if (index + substring.length() < combination.length()) {
	        				index += substring.length()-1;
	        			}
	        			
	        			// If the combination has been fully analyzed, and it can be
	        			// entirely covered by the specifications, print it
			        	if (curString.toString().replaceAll("[^a-zA-Z0-9]", "").length() == combination.length()) {
			        		String word = curString.toString().trim();
			        		if (!associations.contains(String.format("%s: %s", number, word))) {
			        			associations.add(String.format("%s: %s", number, word));
			        		}
			        	}
			        	
	        			break;
	        		}
				}
        		
        		// If no dictionary word can cover the current 
        		if (len == 0)
        		{
        			if (wasDigitUsed) {
        				break;
        			}
        			else {
        				curString.append(normalizedNumber.charAt(index) + " ");
        				wasDigitUsed = true;
        				
	        			// If the combination has been fully analyzed, and it can be
	        			// entirely covered by the specifications, print it
			        	if (curString.toString().replaceAll("[^a-zA-Z0-9]", "").length() == combination.length()) {
			        		String word = curString.toString().trim();
			        		if (!associations.contains(String.format("%s: %s", number, word))) {
			        			associations.add(String.format("%s: %s", number, word));
			        		}
			        	}
        			}
        		}
			}        	
        }
        
        return associations;
	}
	
	/**
	 * Normalize a number, removing any non-digit characters from it
	 * @param number
	 * @return The normalized number
	 */
	private static String normalizeNumber(final String number) {
		StringBuffer normalizedNumber = new StringBuffer();
		for (int i=0; i < number.length(); ++i) {
			if (Character.isDigit(number.charAt(i))) {
				normalizedNumber.append(number.charAt(i));
			}
		}
		
		return normalizedNumber.toString();
	}
	
	/**
	 * Recursively compute all the possible combinations of letters that can be made from a number, according to the provided mapping
	 * @param number The number
	 * @param curIdx Current index (used for the recursive calls)
	 * @param curCombination (reference passed for the recursive calls)
	 * @param combinations The array that will contain all the combinations associated to the number
	 */
	private void numberCombinations(final String number, final int curIdx, AtomicReference<StringBuffer> curCombination, ArrayList<String> combinations) {
		// If we have reached the end of the number string, append the current combination to the list and return
		if (curIdx >= number.length()) {
			combinations.add(curCombination.get().toString());
			
			if (curCombination.get().length() > 1) {
				curCombination.set(curCombination.get().delete(curCombination.get().length()-1, curCombination.get().length()));
			}
			
			return;
		}
		
		char curChar = number.charAt(curIdx);
		if (Character.isDigit(curChar)) {
			int digit = Integer.parseInt("" + number.charAt(curIdx));
			List<Character> mappingByDigit = mapping.get(digit);
			
			for (char ch : mappingByDigit) {
				curCombination.set(curCombination.get().append(ch));
				numberCombinations(number, curIdx+1, curCombination, combinations);
			}
			
			if (curCombination.get().length() > 1) {
				curCombination.set(curCombination.get().delete(curCombination.get().length()-1, curCombination.get().length()));
			}
			else {
				curCombination.set(new StringBuffer());
			}
		}
	}
}
